로딩 중이에요... 🐣
25 검색기능구현(Query String 기반 검색) | ✅ 편저: 코담 운영자
25강 - 검색기능구현(Query String 기반 검색)
기능 - query string✨ 이번 강의 목표
- Query String을 활용한 검색 처리 방식 이해
- 검색어(q 파라미터)를 URL에 포함하여 서버에 전달
- Django ORM을 활용한 필터링(queryset filtering) 구현
- DRF + 무한 스크롤 기반 검색 처리 연동까지 포함
📦 django_instagram/posts/api/urls.py
from django.urls import path
from .views import PostAllListView, PostListView, PostLikeView
from . import views
app_name = "posts_api"
urlpatterns = [
# 전체 게시글 (현재 사용자 + 전체 최신글)
path("all/posts/", PostAllListView.as_view(), name="api_posts"),
# 현재 사용자 + 팔로잉 게시글
path("posts/", PostListView.as_view(), name="api_posts"),
# posts 함수형 리스트 뷰
path("posts/list", views.posts_list_view, name="api_posts_list"),
# 검색 기능 (GET 요청)
path("posts/searchList/", views.posts_search_list, name="api_posts_searchList"),
# 좋아요 추가/취소 (POST 요청)
path("posts/<int:post_id>/post_like/", PostLikeView.as_view(), name="post_like"),
]
urlpatterns
는 각 URL 요청에 어떤 뷰(View)가 응답할지를 정의하는 목록입니다.
path()
는 첫 번째 인자에 URL 경로, 두 번째 인자에 뷰 함수 또는 클래스형 뷰, 세 번째 인자에 URL 별칭(name
)을 지정합니다.이 설정을 통해 프론트엔드에서
/api/posts/
같은 경로로 데이터 요청이 가능합니다.
🧩 django_instagram/posts/api/views.py
✅ 전체 게시글 리스트 뷰 (PostAllListView)
class PostAllListView(generics.ListAPIView):
serializer_class = PostSerializer # 어떤 serializer로 데이터를 가공할지 지정
permission_classes = [permissions.IsAuthenticated] # 인증된 사용자만 접근 가능
def get_queryset(self):
user = get_object_or_404(user_model, pk=self.request.user.id) # 현재 로그인한 유저
search_keyword = self.request.GET.get('q', "") # 검색어 추출
# 현재 사용자 본인의 게시글과 다른 사람의 게시글 분리
user_posts = Post.objects.filter(author=user, caption__icontains=search_keyword)
other_posts = Post.objects.filter(caption__icontains=search_keyword).exclude(author=user)
# 다른 사람의 게시글만 최신순으로 정렬하여 반환
queryset = other_posts.order_by('-author_id', '-created_at')
return queryset
ListAPIView
는 리스트 데이터를 보여주는 Django REST Framework의 클래스입니다.
get_queryset()
은 어떤 데이터를 보여줄지 결정합니다.
caption__icontains
는 대소문자 구분 없이 검색어가 포함된 게시글을 필터링합니다.
def list(self, request, *args, **kwargs):
queryset = self.get_queryset() # 전체 게시글 목록 가져오기
# 페이지 처리 (예: 1페이지당 5개씩)
page = self.request.GET.get('page', 1)
page_size = self.request.GET.get('pageSize', 5)
paginator = Paginator(queryset, page_size)
page_obj = paginator.get_page(page)
# 직렬화 처리
serializer = self.get_serializer(page_obj, many=True, context={'request': request})
login_user_serializer = UserSerializer(request.user, context={'request': request})
return Response({
"posts": serializer.data,
"loginUser": login_user_serializer.data,
"has_next": page_obj.has_next(),
"total_pages": paginator.num_pages
}, status=status.HTTP_200_OK)
페이지네이션은 게시글이 너무 많을 때 화면에 한 번에 다 보여주지 않고, 일정 수만큼씩 나눠서 보여주는 방식입니다.
paginator.get_page(page)
는 지정된 페이지 번호에 해당하는 데이터를 가져옵니다.
has_next
는 다음 페이지가 존재하는지를 알려줍니다.
✅ 사용자 + 팔로잉 게시글 리스트 뷰 (PostListView)
class PostListView(generics.ListAPIView):
serializer_class = PostSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
user = get_object_or_404(user_model, pk=self.request.user.id)
search_keyword = self.request.GET.get('q', "")
following = user.following.all() # 내가 팔로우하는 유저 목록
queryset = Post.objects.filter(
Q(author=user) | Q(author__in=following),
caption__icontains=search_keyword
).annotate(like_count=Count('image_likes')) # 좋아요 수 카운트 추가
return queryset.order_by('-author', '-like_count', '-created_at')
이 뷰는 내가 쓴 게시글 + 내가 팔로우한 유저들의 게시글을 보여줍니다.
게시글 정렬 기준은 작성자 우선 → 좋아요 많은 순 → 최신순 입니다.
.annotate(like_count=...)
를 통해 좋아요 수를 미리 계산해두는 이유는 정렬 시 사용하기 위해서입니다.
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
page = self.request.GET.get('page', 1)
page_size = self.request.GET.get('pageSize', 5)
paginator = Paginator(queryset, page_size)
page_obj = paginator.get_page(page)
serializer = self.get_serializer(page_obj, many=True, context={'request': request})
login_user_serializer = UserSerializer(request.user, context={'request': request})
return Response({
"posts": serializer.data,
"loginUser": login_user_serializer.data,
"has_next": page_obj.has_next(),
"total_pages": paginator.num_pages
}, status=status.HTTP_200_OK)
get_serializer()
는 queryset을 JSON 형태로 바꿔주는 직렬화 과정입니다.클라이언트에
posts
,loginUser
,has_next
,total_pages
라는 구조로 응답을 줍니다.이 구조를 그대로 자바스크립트에서 받아 무한스크롤 처리에 활용합니다.
🧠 무한 스크롤 및 검색 기능 자바스크립트 (loadMorePosts.js)
✅ lodash 스크립트 로드
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
🔰 설명:
lodash는 자바스크립트 유틸리티 라이브러리입니다.
여기서는
_.throttle()
함수를 사용해 스크롤 이벤트 과도 호출을 방지합니다.
✅ 게시글 비동기 로딩 함수 (loadMorePosts)
let page = 1; // 현재 페이지 번호
let loading = false; // 중복 요청 방지를 위한 로딩 플래그
const container = document.querySelector('#postList'); // 게시글이 표시될 영역
let postUrl = "/api/posts/"; // API 요청 URL
const loadMorePosts = async () => {
if (loading) return; // 이미 로딩 중이면 요청 중단
loading = true;
try {
const q = document.querySelector('#q').value.trim(); // 검색어 값 가져오기
const response = await fetch(`${postUrl}?format=json&page=${page}&q=${q}&pageSize=3`); // 게시글 요청
const data = await response.json(); // JSON 파싱
data.posts.forEach(post => {
const postElement = postsHtmlTemplate(post, data.loginUser); // 템플릿을 통해 HTML 생성
container.insertAdjacentHTML('beforeend', postElement); // DOM에 추가
});
if (!data.has_next) {
// 더 이상 페이지가 없으면 스크롤 이벤트 제거
window.removeEventListener('scroll', handleScroll);
}
page += 1; // 다음 페이지로 이동
} catch (error) {
console.error("Error loading posts:", error); // 오류 출력
} finally {
loading = false; // 로딩 상태 해제
}
};
🔰 설명:
이 함수는 서버에서 게시글 데이터를 비동기로 받아오고, 페이지에 렌더링합니다.
has_next
값이 false이면 무한스크롤을 종료합니다.
postsHtmlTemplate()
함수는 서버에서 받은 데이터를 HTML로 변환하는 함수입니다.
✅ 검색 기능 처리 함수
function searchOnEnter(event) {
if (event.key === "Enter") {
event.preventDefault(); // 기본 제출 방지
searchInstagram(); // 검색 실행
}
}
function searchInstagram() {
document.querySelector("#postList").innerHTML = ""; // 기존 게시글 초기화
page = 1; // 첫 페이지부터 시작
window.addEventListener('scroll', handleScroll); // 스크롤 이벤트 등록
loadMorePosts(); // 게시글 로드
}
🔰 설명:
사용자가 검색창에서 Enter를 누르면 기존 목록을 비우고 새로운 검색 결과를 불러옵니다.
페이지 번호도 다시 1로 초기화해야 검색 결과가 처음부터 올바르게 로드됩니다.
✅ 스크롤 이벤트 처리 (handleScroll)
const handleScroll = _.throttle(() => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 10) {
loadMorePosts();
}
}, 200);
🔰 설명:
scrollTop
+clientHeight
는 현재 사용자의 화면 하단 위치를 의미합니다.
scrollHeight - 10
은 전체 문서에서 10px 남았을 때를 뜻합니다.즉, 거의 맨 아래 도달하면 추가 게시글을 불러오도록 합니다.
_.throttle(..., 200)
은 이 이벤트가 0.2초에 한 번만 실행되도록 제한합니다.
✅ 초기 실행 코드
window.addEventListener('scroll', handleScroll); // 스크롤 이벤트 연결
loadMorePosts(); // 첫 게시글 로드
🔰 설명:
- 페이지 진입 시 최초 게시글을 불러오고, 이후 스크롤을 통해 추가 게시글을 자동으로 불러오게 됩니다.
✅ 작동 방식 요약
-
페이지 진입 시
loadMorePosts()
로 첫 게시글 로드 -
사용자가 스크롤을 아래로 내리면
handleScroll()
이 실행되고, 조건을 만족하면 추가 게시글 요청 -
searchOnEnter()
와searchInstagram()
함수로 키워드 검색 시 기존 게시글 초기화 + 새로운 검색 결과 로드 -
has_next
가 false일 경우 더 이상 로딩하지 않도록 스크롤 이벤트를 제거 -
_.throttle()
로 이벤트 과도 실행을 방지하여 성능을 높임
📌 이 구조를 통해 검색 기능과 무한 스크롤을 동시에 구현하며, 사용자 경험을 개선할 수 있습니다.
✅ 정리
- 검색은 query string 방식으로 이루어짐 (
?q=검색어
) - Django DRF 뷰 내부에서
caption__icontains
를 통해 검색 처리 - JS에서는 검색어를 input으로 받아 URL에 포함해 fetch 요청 전송
- 무한 스크롤 방식과 검색 기능을 결합하여 UX 향상
👉 다음 강의는 배포 관련 내용을 다룹니다.